type ArgsFn<Args> = Args extends any[] ? (...args: Args) => void : (event: Args) => void

export interface EmitEventMap {}

export class Emit<EventMap extends {} = EmitEventMap> {
  protected store = new Map<PropertyKey, Set<{ once: boolean; fn: (...args: any[]) => void }>>()

  protected queue(event: any) {
    if (!this.store.has(event)) {
      this.store.set(event, new Set())
    }
    return this.store.get(event)!
  }

  on<K extends keyof EventMap>(event: K, fn: ArgsFn<EventMap[K]>, once = false): this {
    this.queue(event).add({ once, fn: fn as any })
    return this
  }

  once<K extends keyof EventMap>(event: K, fn: ArgsFn<EventMap[K]>): this {
    return this.on(event, fn, true)
  }

  off<K extends keyof EventMap>(event: K, fn: ArgsFn<EventMap[K]>): this {
    const queue = this.store.get(event)
    if (queue) {
      queue.forEach((item) => {
        item.fn == fn && queue.delete(item)
      })
      !queue.size && this.store.delete(event)
    }
    return this
  }

  emit<K extends keyof EventMap>(event: K, ...args: Parameters<ArgsFn<EventMap[K]>>): this {
    const queue = this.store.get(event)
    if (queue) {
      queue.forEach((item) => {
        item.fn(...args)
        item.once && queue.delete(item)
      })
      !queue.size && this.store.delete(event)
    }
    return this
  }

  dispose() {
    this.store.clear()
  }
}

type StorageEventMap<T extends EmitEventMap> = {
  [K in keyof T]: [newValue: T[K], oldValue: T[K] | undefined]
}

const localstorage = chrome?.storage?.local

export class StorageEmit<T extends EmitEventMap, M extends StorageEventMap<any> = StorageEventMap<T>> extends Emit<M> {
  protected initialized = false

  #onChange: (changes: { [name: string]: chrome.storage.StorageChange }, areaName: string) => void

  constructor(protected initial: T) {
    super()

    this.#onChange = (changes, areaName) => {
      areaName === 'local' &&
        (Object.keys(changes) as (keyof T)[]).forEach((key) => {
          if (key in this.initial) {
            const { newValue, oldValue } = (changes as any)[key] as chrome.storage.StorageChange

            if (JSON.stringify(this.initial[key]) !== JSON.stringify(newValue)) {
              this.initial[key] = newValue
              // @ts-ignore
              this.emit(key, newValue, oldValue)
            }
          }
        })
    }

    if (localstorage) {
      chrome.storage.onChanged.addListener(this.#onChange)

      localstorage
        .get(initial)
        .then((data) => {
          const keys = Object.keys(data) as (keyof T)[]
          keys.forEach((key) => {
            const newValue = (data as T)[key]
            if (JSON.stringify(this.initial[key]) !== JSON.stringify(newValue)) {
              this.initial[key] = structuredClone(newValue)
              // @ts-ignore
              this.emit(key, newValue)
            }
          })
        })
        .finally(() => {
          this.initialized = true
        })
    }
  }

  immediate<K extends keyof T>(event: K, fn: ArgsFn<M[K]>, once = false): this {
    if (this.initialized) {
      fn(this.initial[event], undefined)
    }
    ;(!this.initialized || !once) && this.on(event, fn, once)
    return this
  }

  dispose() {
    super.dispose()
    localstorage && chrome.storage.onChanged.removeListener(this.#onChange)
  }

  get<K extends keyof T>(key: K) {
    return structuredClone(this.initial[key])
  }

  set<K extends keyof T>(key: K, value: T[K]) {
    const oldValue = JSON.stringify(this.initial[key])
    if (oldValue !== JSON.stringify(value)) {
      this.initial[key] = structuredClone(value)
      localstorage?.set({ [key]: value })
    }
  }
}
